本文由 简悦 SimpRead 转码, 原文地址 https://juejin.im/post/5af952fdf265da0b9e652de3
什么是 SPI?
SPI 全称为 (Service Provider Interface) ,是 JDK 内置的一种服务提供发现机制。SPI 是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是 java.sql.Driver 接口,其他不同厂商可以针对同一接口做出不同的实现,mysql 和 postgresql 都有不同的实现提供给用户,而 Java 的 SPI 机制可以为某个接口寻找服务实现。
类图中,接口对应定义的抽象 SPI 接口;实现方实现 SPI 接口;调用方依赖 SPI 接口。
SPI 接口的定义在调用方,在概念上更依赖调用方;组织上位于调用方所在的包中;实现位于独立的包中。
当接口属于实现方的情况,实现方提供了接口和实现,这个用法很常见,属于 API 调用。我们可以引用接口来达到调用某实现类的功能。
Java SPI 应用实例
当服务的提供者提供了一种接口的实现之后,需要在 classpath 下的 META-INF/services / 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个 jar 包(一般都是以 jar 包做依赖)的 META-INF/services / 中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK 中查找服务实现的工具类是:java.util.ServiceLoader。
SPI 接口
1 | public interface ObjectSerializer { |
定义了一个对象序列化接口,内有三个方法:序列化方法、反序列化方法和序列化名称。
SPI 具体实现
1 | public class KryoSerializer implements ObjectSerializer { |
使用 Kryo 的序列化方式。Kryo 是一个快速高效的 Java 对象图形序列化框架,它原生支持 java,且在 java 的序列化上甚至优于 google 著名的序列化框架 protobuf。
1 | public class JavaSerializer implements ObjectSerializer { |
Java 原生的序列化方式。
增加 META-INF 目录文件
Resource 下面创建 META-INF/services 目录里创建一个以服务接口命名的文件
1 | com.blueskykong.javaspi.serializer.KryoSerializer |
Service 类
1 | @Service |
获取定义的序列化方式,且只取第一个(我们在配置中写了两个),如果找不到则返回 Java 原生序列化方式。
测试类
1 | @Autowired |
测试用例通过,且输出kryoSerializer
。
SPI 的用途
数据库 DriverManager、Spring、ConfigurableBeanFactory 等都用到了 SPI 机制,这里以数据库 DriverManager 为例,看一下其实现的内幕。
DriverManager 是 jdbc 里管理和注册不同数据库 driver 的工具类。针对一个数据库,可能会存在着不同的数据库驱动实现。我们在使用特定的驱动实现时,不希望修改现有的代码,而希望通过一个简单的配置就可以达到效果。 在使用 mysql 驱动的时候,会有一个疑问,DriverManager 是怎么获得某确定驱动类的?我们在运用 Class.forName(“com.mysql.jdbc.Driver”) 加载 mysql 驱动后,就会执行其中的静态代码把 driver 注册到 DriverManager 中,以便后续的使用。
在 JDBC4.0 之前,连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")
这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而 JDBC4.0 之后不需要Class.forName
来加载驱动,直接获取连接即可,这里使用了 Java 的 SPI 扩展机制来实现。
在 java 中定义了接口 java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
mysql
在 mysql-connector-java-5.1.45.jar 中,META-INF/services 目录下会有一个名字为 java.sql.Driver 的文件:
1 | com.mysql.jdbc.Driver |
pg
而在 postgresql-42.2.2.jar 中,META-INF/services 目录下会有一个名字为 java.sql.Driver 的文件:
1 | org.postgresql.Driver |
用法
1 | String url = "jdbc:mysql://localhost:3306/test"; |
上面展示的是 mysql 的用法,pg 用法也是类似。不需要使用Class.forName("com.mysql.jdbc.Driver")
来加载驱动。
Mysql DriverManager 实现
上面代码没有了加载驱动的代码,我们怎么去确定使用哪个数据库连接的驱动呢?这里就涉及到使用 Java 的 SPI 扩展机制来查找相关驱动的东西了,关于驱动的查找其实都在 DriverManager 中,DriverManager 是 Java 中的实现,用来获取数据库连接,在 DriverManager 中有一个静态代码块如下:
1 | static { |
可以看到其内部的静态代码块中有一个loadInitialDrivers
方法,loadInitialDrivers
用法用到了上文提到的 spi 工具类ServiceLoader
:
1 | public Void run() { |
遍历使用 SPI 获取到的具体实现,实例化各个实现类。在遍历的时候,首先调用driversIterator.hasNext()
方法,这里会搜索 classpath 下以及 jar 包中所有的 META-INF/services 目录下的 java.sql.Driver 文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类。
总结
SPI 机制在实际开发中使用得场景也有很多。特别是统一标准的不同厂商实现,当有关组织或者公司定义标准之后,具体厂商或者框架开发者实现,之后提供给开发者使用。
本文代码: https://github.com/keets2012/Spring-Boot-Samples/tree/master/java-spi